Java JVM学习-创建一个对象
创建对象的步骤
平时创建一个对象只需要 new。然而对象的创建到底经历了哪些呢?实际上只不过仅仅的 3步就完成了。先来看看完整的创建过程,再来一步一步的分析。

加载类元信息(类加载检查)
要创建对象肯定首先要知道是什么、有没有。所以首先就是找到对象的类信息。类信息都是放到方法区的。
从这里看出类信息放到方法区是很有必要的,因为每个线程每个方法都可能需要这些信息。

虚拟机遇到一条 new 指令,首先去检查这个指令的参数能否在 Metaspace 的常量池中定位到一个类的符号引用,并且检查这个 符号引用代表的关是否已经被加载,解析和初始化(即判断类元信息是否存在)。
如果没有,那么在双亲委派模式下,使用当前类加载器以 ClassLoader + 包名 + 类名 为 Key进行查找对应的 .class 文件。如果没有找到文件,则抛出 ClassNotFoundException 异常如果找到,则进行类加载,并生成对应的 Class类对象
为对象分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。 而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的

如果内存规整(指针碰撞法)
如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Pointer)来为对象分配内存。意思是所有用过的内存在一边 ,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象 大小相等的距离罢了。
如果垃圾收集器选择的是 Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有 compact(整理)过程的收生器时,就是使用指针碰撞。
如果内存不规整(空闲列表)
如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。
意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为 “空闲列表(Free List)”。
补充:处理并发安全问题
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
都知道对象是放到 Java 堆中的,同时对象是随时都在创建的,当多个线程运行的时候就有可能把对象放到同一个地方,那么肯定就会有线程拿到不是他想要的对象。
1、CAS + 失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
2、TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。虚拟机是否使用根据 -XX:+/-UseTLAB 参数设置。
如下图:

所以虽然 Java堆是线程共享的,但也有可能一些内存实际上是线程独享的。
补充:对象分配过程 TLAB
什么是 TLAB?
- 从内存模型而不是垃圾收集的角度,对 Eden 区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内。
- 多线程同时分配内存时,使用 TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

为什么有 TLAB(Thread Local Allocation Buffer)?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在 JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
TLAB 的再说明:
尽管不是所有的对象实例都能够在 TLAB中成功分配内存,但 JVM 确实是将 TLAB 作为内存分配的首选。
在程序中,开发人员可以通过选项 -XX:UseTLAB 设置是否开启 TLAB空间。
默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden空间的 1%,可以通过选项 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden空间的百分比大小。
一旦对象在 TLAB 空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden空间中分配内存。

对象执行初始化
现在对象的位置知道了,空间也分配了。但是里面还是空荡荡的一块。现在就要创造出内容来。

将对象的所属类(即类的元数据信息)、对象的 HashCode和对象的 GC信息、锁信启等数据存储在对象的对象头中。这个过程的具体设置方式取决于 JVM实现。
设置属性的零值
因为在 TLAB模式下,初始化属性的零值已经设置过了,所以这里有可能不需要设置。只有设置了值,我们才能在调用的时候才能获取到正确的值。
如下所示:
public class Test {
int data;
String data2;
boolean data3;
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.data1);
System.out.println(test.data2);
System.out.println(test.data3);
}
}
打印出来分别是 0、null、false。
之所以能打印出来这些,就是因为这里的初始化。如果没有这一步有可能data1打印出来的就不是0。data2打印出来的就更加不知道是什么了。
填充对象头信息
对象里面要存必要的东西,比如对象类型信息。如果是 Java数组还要记录数组的长度。以及一些其他信息,如下图所示:

具体对象头包含了啥,看下面对象头那节
执行 init 进行初始化
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
因此一般来说初始化由字节码中是否跟随有 invokespecial 指令所决定,
new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化。这样一个真正可用的对象才算完全创建出来。
这一步主要初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
补充:对象的内存布局
在 HotSpot 虚拟机中,一个对象在内存中的布局包含三块内容:对象头、实例数据、对其填充。